# BloggerImporter plugin for Movable Type
# Author: Six Apart (http://www.sixapart.com)
# Released under the Artistic License
#
# $Id$

package BloggerImporter::Import;
use strict;

use base qw(MT::ErrorHandler);
use MT::Util qw( format_ts );
use MT::I18N qw( substr_text );

sub import_contents {
    my $self = shift;
    my %param = @_;

    return $self->error(MT->translate("please input email/pass."))
        unless ($param{google_mail} && $param{google_pass});

    require LWP::UserAgent;
    my $ua = LWP::UserAgent->new;
    my $auth_token = $self->google_login($ua, \%param) or return;

    require XML::LibXML;
    my $parser = XML::LibXML->new;

    # Get user profile.
    my $prof_url = 'http://www.blogger.com/feeds/default/blogs';
    my $prof_req = HTTP::Request->new(GET => $prof_url);
    $prof_req->header('Authorization', "GoogleLogin auth=$auth_token");
    my $prof_res = $ua->request($prof_req);
    my $prof_doc = $parser->parse_string($prof_res->content);
    my @blogger_ids;
    my $bloggers = $prof_doc->findnodes("//*[local-name()='feed']/*[local-name()='entry']");
    for my $blogger (@$bloggers) {
        if ((scalar(@$bloggers) > 1)
           && ($param{blogger_url})) {
            my $links = $blogger->findnodes("*[local-name()='link']");
            my $is_skip;
            my $blogger_url = 'http://' . $param{blogger_url} . '.blogspot.com/';
            for my $link (@$links) {
                next unless $link->findvalue('@rel') eq 'alternate';
                next if $link->findvalue('@href') eq $blogger_url;
                $is_skip = 1;
            }
            next if $is_skip; 
        }
        my $author = $blogger->findvalue("*[local-name()='author']/*[local-name()='name']/text()");
        my $id = $blogger->findvalue("*[local-name()='id']/text()");
        my ($blogger_id) = $id =~ m/blog-(\w+)/i;
        my %blogger_param;
        $blogger_param{author} = $author;
        $blogger_param{id} = $blogger_id;
        push @blogger_ids, \%blogger_param;
    }

    require MT::App::CMS;
    my $class = MT::App::CMS->_load_driver_for('entry');
    require MT::Comment;
    my $blog = $param{Blog};
    my $cb = $param{Callback};

    my($mt_path, $root_path, $root_url, $relative_path);
    if (my $p = delete $param{'mt_site_path'}) {
        $root_path = $root_url = '%r';
    } else {
        $root_path = $root_url = '%a';
    }

    if ($root_path && $root_url) {
        $relative_path = delete $param{'mt_extra_path'};
        my $path = $root_path;
        if ($relative_path) {
            if ($relative_path =~ m!\.\.|\0|\|!) {
                return $class->error(MT->translate(
                    "Invalid extra path '[_1]'", $relative_path));
            }
            $path = File::Spec->catdir($path, $relative_path);
        }

        ## $path should be local path to file now (we don't create/upload files nor mkpath)

        my $url = $path;
        $url =~ s!\\!/!g;

        $mt_path = $path;
    }

    # create request for posted entries.
    for my $blogger_param (@blogger_ids) {
        my %post_id_matrix;
        my $blogger_id = $blogger_param->{id};
        my $blogger_author = $blogger_param->{author};

        my $posts_url = "http://www.blogger.com/feeds/$blogger_id/posts/default";
        my $import_xml = $self->_get_feed_xml($posts_url, $ua, $auth_token, $parser);
        my $import_feeds = $parser->parse_string($import_xml);
        my $entries = $import_feeds->findnodes("//*[local-name()='entry']");
        for my $entry (@$entries) {
            my $post = $class->new;
            $post->blog_id($blog->id);
            my $is_draft =
                $entry->findvalue("*[local-name()='control']/*[local-name()='draft']/text()") eq 'yes' ? 1 : 0;
            $post->status($is_draft ? MT::Entry::HOLD : MT::Entry::RELEASE);
            my $author_name = $entry->findvalue("*[local-name()='author']/*[local-name()='name']/text()");
            $post->author_id($self->_get_author_id($cb, \%param, $author_name));
            $post->title($entry->findvalue("*[local-name()='title']/text()"));
            $post->authored_on(MT::Util->iso2ts($entry->findvalue("*[local-name()='published']/text()")));
            $post->modified_on(MT::Util->iso2ts($entry->findvalue("*[local-name()='updated']/text()")));
            my $content = $entry->findvalue("*[local-name()='content']/text()");
            $post->text($content);

            my $links = $entry->findnodes("*[local-name()='link']");
            for my $link (@$links) {
                if ($link->findvalue('@rel') eq 'alternate') {
                    my $alternate = $link->findvalue('@href');
                    my ($base, $path, $ext) = File::Basename::fileparse($alternate, "\.[^.]*");
                    $post->basename($base);
                }
            }

            # save assets
            require MT::Asset;
            require File::Basename;
            my @img_files;
            while ($content =~ m/(<img.*?>)/ig) {
                my $img = $1;
                my %img_param;
                ($img_param{src}) = $img =~ m/src="(.*?)"/i;
                ($img_param{title}) = $img =~ m/title="(.*?)"/i;
                push @img_files, \%img_param;
            } 
            ASSETS:
            for my $file (@img_files) {
                my $path = $file->{src};
                next ASSETS unless $path;
                my $local_basename = File::Basename::basename($path);
                my $ext = (File::Basename::fileparse($path, qr/[A-Za-z]+$/))[2];
                my $asset_values;
#                $asset_values->{'blog_id'} = $blog->id;
                $asset_values->{'url'} = $file->{src};
                $asset_values->{'label'} = $file->{title};
                $asset_values->{'file_name'} = $local_basename;
                $asset_values->{'file_ext'} = $ext;
                $asset_values->{'file_path'} = File::Spec->catfile($mt_path, $local_basename);

                my $asset_pkg = MT::Asset->handler_for_file($local_basename);
                # Now save the asset.
                my $asset = $asset_pkg->new();
                $asset->blog_id($blog->id);
                $asset->set_values($asset_values);
                $cb->(MT->translate("Saving asset ('[_1]')...", $asset->label));
                if ($asset->save) {
                    $cb->(MT->translate("ok (ID [_1])", $asset->id) . "\n");
                } else {
                    $cb->(MT->translate("failed") . "\n");
                    die MT->translate(
                        "Saving entry failed: [_1]", $asset->errstr);
                }
            }

            my @cat_terms;
            my $categories = $entry->findnodes("*[local-name()='category']");
            for my $cat (@$categories) {
                push @cat_terms, $cat->findvalue('@term');
            }
            $post->add_tags(@cat_terms);

            # save the entry.
            $cb->(MT->translate("Saving entry ('[_1]')...", MT::I18N::utf8_off($post->title)));
            if ($post->save) {
                $cb->(MT->translate("ok (ID [_1])", $post->id) . "\n");
            } else {
                $cb->(MT->translate("failed") . "\n");
                die MT->translate(
                    "Save failed: [_1]", $post->errstr);
            }
            my $blogger_post_links = $entry->findnodes("*[local-name()='link']");
            for my $link (@$blogger_post_links) {
                next unless $link->findvalue('@rel') eq 'alternate';
                my $href = $link->findvalue('@href');
                $post_id_matrix{$href} = $post->id;
            }

            # save categories.
            my %cat_ids;
            my $primary_cat_id;
            for my $term (@cat_terms) {
                my $cat_class = MT::App::CMS->_load_driver_for('category');
                my $cat = $cat_class->load(
                    { label => $term,
                      blog_id => $blog->id });
                if (defined $cat) {
                    $cat_ids{$cat->id} = 1;
                    $primary_cat_id = $cat->id unless $primary_cat_id;
                }
            }
            $primary_cat_id = $param{DefaultCategoryID} unless $primary_cat_id;
            if ($primary_cat_id) {
                my $place = MT::Placement->new;
                $place->is_primary(1);
                $place->entry_id($post->id);
                $place->blog_id($blog->id);
                $place->category_id($primary_cat_id);
                $place->save
                    or die MT->translate(
                        "Saving placement failed: [_1]", $place->errstr);
                delete $cat_ids{$primary_cat_id};
            }

            for my $cat_id (keys %cat_ids) {
                my $place = MT::Placement->new;
                $place->is_primary(0);
                $place->entry_id($post->id);
                $place->blog_id($blog->id);
                $place->category_id($cat_id);
                $place->save
                    or die MT->translate(
                        "Saving placement failed: [_1]", $place->errstr);
            }
        }

        # save comments.
        my $comments_url = "http://www.blogger.com/feeds/$blogger_id/comments/default";
        my $imported_comments_xml = $self->_get_feed_xml($comments_url, $ua, $auth_token, $parser);
        my $imported_comments = $parser->parse_string($imported_comments_xml);
        my $blogger_comments = $imported_comments->findnodes("//*[local-name()='entry']");
        COMMENTS:
        for my $blogger_comment (@$blogger_comments) {
            my $cmt = MT::Comment->new;
            $cmt->blog_id($blog->id);
            $cmt->author($blogger_comment->findvalue("*[local-name()='author']/*[local-name()='name']/text()"));
            $cmt->created_on(MT::Util->iso2ts($blogger_comment->findvalue("*[local-name()='published']/text()")));
            $cmt->modified_on(MT::Util->iso2ts($blogger_comment->findvalue("*[local-name()='updated']/text()")));
            $cmt->text($blogger_comment->findvalue("*[local-name()='content']/text()"));
            $cmt->approve;

            my $blogger_post_links = $blogger_comment->findnodes("*[local-name()='link']");
            for my $link (@$blogger_post_links) {
                next unless $link->findvalue('@rel') eq 'alternate';
                my $href = $link->findvalue('@href');
                $href =~ s/(\?.*?$)//g;
                my $post_id = $post_id_matrix{$href};
                if ($post_id) {
                    $cmt->entry_id($post_id);
                } else {
                    my $created_on_fmt = format_ts('%b %e, %Y', $cmt->created_on(), $blog, undef);
                    my $text_head = substr_text( $cmt->text(), 0, 30 ) . '...';
                    my $details = 'Posted by ' . $cmt->author() . ' at ' . $created_on_fmt . ': ' . $text_head;
                    $cb->("importing comment failed: permalink ($href) is not found. $details.". "\n");
                    next COMMENTS;
                }
            }
            $cb->(MT->translate("Creating new comment (from '[_1]')...", $cmt->author));
            if ($cmt->save) {
                $cb->(MT->translate("ok (ID [_1])", $cmt->id) . "\n");
            } else {
                $cb->(MT->translate("failed") . "\n");
                die MT->translate(
                    "Saving comment failed: [_1]", $cmt->errstr);
            }
        }
    }
    return 1;
}

sub _get_author_id {
    my $self = shift;
    my ($cb, $param, $value) = @_;

    my $author = exists $param->{ImportAs} ? $param->{ImportAs} : '';
    unless ($author) {
        require MT::BasicAuthor;
        $author = MT::BasicAuthor->load({ name => $value });
        unless (defined $author) {
            my $parent_author = exists $param->{ImportAs} ? '' : $param->{ParentAuthor};
            my $pass = $param->{NewAuthorPassword};
            $author = MT::Author->new;
            $author->created_by($parent_author->id) if defined $parent_author;
            $author->name($value);
            $author->email('');
            $author->type(MT::Author::AUTHOR());
            if ($pass) {
                $author->set_password($pass);
            } else {
                $author->password('(none)');
            }
            $cb->(MT->translate("Creating new user ('[_1]')...", $value));
            if ($author->save) {
                $cb->(MT->translate("ok") . "\n");
            } else {
                $cb->(MT->translate("failed") . "\n");
                die MT->translate(
                    "Saving user failed: [_1]", $author->errstr);
            }
            $cb->(MT->translate("Assigning permissions for new user..."));
            require MT::Role;
            require MT::Association;
            my $role = MT::Role->load_by_permission('post');
            if ($role) {
                my $assoc;
                if ($assoc = MT::Association->link($author => $role => $param->{Blog})) {
                    $cb->(MT->translate("ok") . "\n");
                } else {
                    $cb->(MT->translate("failed") . "\n");
                    die MT->translate(
                         "Saving permission failed: [_1]", $assoc->errstr);
                }
            }
        }
    }
    defined $author ? $author->id : undef;
}

sub google_login {
    my $self = shift;
    my ($ua, $param) = @_;
    my $google_url = 'https://www.google.com/accounts/ClientLogin';
    my %login_param;
    $login_param{Email}   = $param->{google_mail};
    $login_param{Passwd}  = $param->{google_pass};
    $login_param{source}  = 'exampleCo-exampleApp-1';
    $login_param{service} = 'blogger';
    my $login_res = $ua->post($google_url, \%login_param);
    my $auth_token;
    if ($login_res->code == 200) {
        ($auth_token) = $login_res->content =~ m/Auth=(.+)(\s+|$)/i;
    } elsif ($login_res->code == 403) {
        return $self->error(MT->translate('403 Forbidden; authentication failed.'));
    } else {
        return $self->error(MT->translate("An error occurred"));
    }
    return $auth_token;
}

sub _get_feed_xml {
    my $self = shift;
    my ($url, $ua, $auth_token, $parser) = @_;

    my $return_xml;
    while ($url) {
        my $req = HTTP::Request->new(GET => $url);
        $req->header('Authorization', "GoogleLogin auth=$auth_token");
        my $res = $ua->request($req);
        my $xml = $res->content;
        $xml =~ s!<\?xml\s+.*?\?>!!;
        $xml =~ s!<\?xml-stylesheet\s+.*?\?>!!;
        $return_xml .= $xml;

        # check more entries.
        my $feed = $parser->parse_string($res->content);
        my $links = $feed->findnodes("//*[local-name()='feed']/*[local-name()='link']");
        $url = '';
        for my $link (@$links) {
            $url = $link->findvalue('@href') if $link->findvalue('@rel') eq 'next';
        }
    }
    return '<BloggerImporter>' . $return_xml . '</BloggerImporter>';
}

sub get_param {
    my $class = shift;
    my ($blog_id) = @_;

    my $blog = MT::Blog->load($blog_id) or return $class->error(MT->translate('No Blog'));

    my $param = { blog_id => $blog_id };
    my $label_path;
    if ($blog->column('archive_path')) {
        $param->{enable_archive_paths} = 1;
        $label_path = MT->translate('Archive Root');
    } else {
        $label_path = MT->translate('Site Root');
    }
    $param->{missing_paths} =
           ((defined $blog->site_path || defined $blog->archive_path) 
         && (-d $blog->site_path || -d $blog->archive_path)) ? 0 : 1;
    $param;
}

1;

__END__
